﻿using System;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq.Expressions;

namespace Postnuclear.Dependency
  {
  /// <summary>
  /// Stores information about binding:
  /// - action which performs updating of the target;
  /// - dependency tree for the source expression;
  /// Connects dependency tree with update action.
  /// </summary>
  /// <typeparam name="TMember">The type of the member.</typeparam>
  /// <typeparam name="TSource">The type of the source.</typeparam>
  internal sealed class ExpressionBinding<TMember, TSource>
    where TSource : TMember
    {
    #region Public

    // Constructors

    /// <summary>
    /// Initializes a new instance of the <see cref="ExpressionBinding{TMember, TSource}"/> class.
    /// </summary>
    /// <param name="target">The target property expression.</param>
    /// <param name="source">The source value calculation expression.</param>
    /// <exception cref="InvalidOperationException">If target has incorrect format.</exception>
    public ExpressionBinding(Expression<Func<TMember>> target, Expression<Func<TSource>> source)
      {
      Contract.Requires<ArgumentNullException>(target != null);
      Contract.Requires<ArgumentNullException>(source != null);
      Contract.Requires<ArgumentException>(target.IsSimpleMemberAccessExpression());

      target = target.Simplify();

      if (target.Body.NodeType == ExpressionType.Constant)
        {
        throw new InvalidOperationException("Binding to static fields or local variables is not allowed.");
        }

      Target = new WeakReference(FindMemberConstant(target));
      if (Target.Target == null)
        {
        throw new InvalidOperationException("Binding to static members is not allowed.");
        }

      IDisposed disposableTarget = Target.Target as IDisposed;
      if (disposableTarget != null)
        {
        _isDisposed = () => disposableTarget.Disposed;
        }
      else
        {
        _isDisposed = () => false;
        }

      Source = source.Simplify().Weaken(Target.Target).Observe();
      Source.ValueChanged += OnSourceValueChanged;

      Update = target.Weaken(Target.Target).AssignTo(() => Source.Value).Compile();
      Update();
      }

    #endregion

    #region Private

    // Properties

    private bool IsDisposed
      {
      get
        {
        return _isDisposed();
        }
      }

    // Methods

    private object FindMemberConstant(Expression<Func<TMember>> target)
      {
      MemberExpression memberExpression = (MemberExpression)target.Body;
      while (memberExpression.Expression != null && memberExpression.Expression.NodeType == ExpressionType.MemberAccess)
        {
        memberExpression = (MemberExpression)memberExpression.Expression;
        }

      if (memberExpression.Expression == null)
        {
        return null;
        }

      if (memberExpression.Expression.NodeType != ExpressionType.Constant)
        {
        throw new InvalidOperationException("Binding target owner should be constant or read-only field.");
        }

      return ((ConstantExpression)memberExpression.Expression).Value;
      }

    [DebuggerNonUserCode]
    private void OnSourceValueChanged(object sender, EventArgs e)
      {
      object target = Target.Target;

      if (Target.IsAlive && !IsDisposed)
        {
        Update();
        }
      else
        {
        Source.ValueChanged -= OnSourceValueChanged;
        }

      GC.KeepAlive(target); // if target is alive we want to be sure it's not garbage collected during update
      }

    // Fields

    private readonly WeakReference Target;

    private readonly IValueProvider<TSource> Source;

    private readonly Action Update;

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly Func<bool> _isDisposed;

    #endregion
    }
  }